jQuery.fn.extend({
    create3DCarousel: function (/*options*/) {
        //config
        var options = {
            vanishingPoint: arguments[0].vanishingPoint,
            speed: arguments[0].speed,
            cameraX_Angle: typeof(arguments[0].cameraX_Angle) != "undefined" ? arguments[0].cameraX_Angle*Math.PI/180 : 0
        }

        //3DCarousel class
        $3DCarousel = function (jEl) {
            var parent = this;

            //Extra Class
            /* NODE */
            $Node = function (/*node, x, y, z*/) {
                var self = this;
                /*fields*/
                this.node = arguments[0].node; //DOM node
                this.nodeIMG = { //CONST
                    width: self.node.find("img:first").width(),
                    height: self.node.find("img:first").height()
                }
                this.x = arguments[0].x;
                this.y = arguments[0].y; //CONST
                this.z = arguments[0].z;
                this.angle_TO_FRONT = arguments[0].angle_TO_FRONT;
                this.depth = 10000 - Math.round(Math.abs(this.z));
                this.timer;

                /*methods*/
                //calculate the item dimension relate to its position(z-depth)
                this.calculate = function () {
                    /*caculation*/
                    //Positioning
                    var sinX = Math.cos((-90-this.angle_TO_FRONT)*Math.PI/180);
                    var cosX = Math.sin((-90-this.angle_TO_FRONT)*Math.PI/180);
                    this.z = parent.Radius*(1+cosX);
                    this.depth = 10000 - Math.round(Math.abs(this.z));
                    this.scalingRatio = 1 - this.z/parent.vanishingPoint;
                    this.x = parent.Radius*sinX/this.scalingRatio;
                    /*Scaling*/
                    this.surfaceScalingRatio = Math.pow(this.scalingRatio, 2);
                    //LIs
                    this.node.width( Math.round( this.surfaceScalingRatio*this.nodeIMG.width ) );
                    this.node.height( Math.round( this.surfaceScalingRatio*this.nodeIMG.height ) );
                    //IMG
                    this.node.find("img:first").css("width", Math.round( this.surfaceScalingRatio*this.nodeIMG.width ) );
                    this.node.find("img:first").css("height", Math.round( this.surfaceScalingRatio*this.nodeIMG.height ) );
                }
                //output to browser: coords of viewport is top:0, left:0
                this.outputToScreen = function () {
                    //TODO
                    var Y_Offset = parent.cameraY_OffSet*(this.z/(parent.vanishingPoint-this.z));
                    this.node.css({
                        left: parent.Radius + this.x - this.node.find("img:first").width()/2 + "px",
                        top: this.y - Y_Offset - this.node.find("img:first").height()/2 + "px",
                        zIndex: this.depth
                    });
                }
                /* 
                /* choose CLOCWISE as the DEFAULT ROTATION in case
                /* rotate LEFT OR RIGHT are both possible
                */
                //CORE function to move NODE : move node to any angle value
                this.moveNodeToAngle = function (angle) {
                    var node = $(this.node);
                    //angle_TO_FRONT + offset*i until = |rangeAngle|
                    //with i = rangeAngle/|rangeAngle|
                    this.timer = setInterval(function () {
                        if ( self.angle_TO_FRONT >= 180 ) {
                            self.angle_TO_FRONT -= 360;
                        }
                        if ( self.angle_TO_FRONT <= -180 ) {
                            self.angle_TO_FRONT += 360;
                        }
                        var rangeAngle = Math.abs(angle - self.angle_TO_FRONT);
                        if ( rangeAngle > 180 ) {
                            rangeAngle = -angle + self.angle_TO_FRONT;
                        }
                        else if ( rangeAngle < 180 ) {
                            rangeAngle = angle - self.angle_TO_FRONT;
                        }
                        else {
                            rangeAngle = Math.abs(angle - self.angle_TO_FRONT)
                        }
                        var i = rangeAngle/Math.abs(rangeAngle);
                        if ( self.angle_TO_FRONT == angle ) {
                            parent.timerControl.collectGarbageTimer();
                            clearInterval(self.timer);
                        }
                        else {
                            if ( Math.abs(angle-self.angle_TO_FRONT) < parent.speed ) {
                                restAngle = Math.abs(angle-self.angle_TO_FRONT);/*key*/
                                self.angle_TO_FRONT += restAngle*i;
                            }
                            else {
                                self.angle_TO_FRONT += parent.speed*i;
                            }
                            self.calculate();
                            self.outputToScreen();
                        }
                    }, 1);

                    parent.timerControl.groupTimer.push(this.timer);

                }
                //Make the NODE become larger
                this.shrink = function () {
                    parent.isZoom = true;
                    //LIs
                    this.node.animate({
                        width: Math.round( parent.scale*self.node.width() ),
                        height: Math.round( parent.scale*self.node.height() ),
                        left: parent.Radius + self.x - self.perspectiveOffSet - parent.scale*self.node.width()/2 + "px",
                        top: self.y - parent.scale*self.node.height()/2 + "px"
                    }, "normal", "swing");
                    //IMG
                    this.node.find("img:first").animate({
                        width: Math.round( parent.scale*this.node.width() ),
                        height: Math.round( parent.scale*this.node.height() )
                    }, "normal", "swing");
                }
                //Revert the NODE to its previous state (before shrink)
                this.revert = function (callback) {
                    //LIs
                    this.node.animate({
                        width: Math.round( self.scalingRatio*self.nodeIMG.width ),
                        height: Math.round( self.scalingRatio*self.nodeIMG.height ),
                        left: parent.Radius + self.x - self.perspectiveOffSet - self.nodeIMG.width/2 + "px",
                        top: self.y - self.nodeIMG.height/2 + "px"
                    }, "normal", "swing");
                    //IMG
                    this.node.find("img:first").animate({
                        width: Math.round( self.scalingRatio*this.nodeIMG.width ),
                        height: Math.round( self.scalingRatio*self.nodeIMG.height )
                    }, "normal", "swing", function () {
                        parent.isZoom = false;
                        if ( typeof(callback) != "undefined" ) {
                            callback();
                        }
                    });
                }
                //node event
                this.node.find("a:first").bind("click", function (evt) {
                    if ( self.angle_TO_FRONT != 0 && self.angle_TO_FRONT != 360 ) {
                        parent.queue.rotateToFront(self);
                    }
                    return false;
                });
            }
            /* QUEUE */
            $Queue = function (/*$NodeList:Array*/) {
                var self = this;
                /*field*/
                this.queue = typeof(arguments[0]) != "undefined" ? arguments[0] : [];
                this.head = this.queue.length > 0 ? this.queue[0] : null;

                //return the number of items in queue
                this.length = function (item/*type:$Node*/) {
                    return this.queue.length;
                }
                //return the first item out of the queue
                this.serve = function () {
                    var returnItem = this.queue[0];
                    this.queue.splice(0,1);
                    return returnItem;
                }
                //append item to the end of a queue, no return
                this.append = function (item/*type:$Node*/) {
                    this.queue.push(item);
                }
                //prepend item to the front of a queue, no return
                this.prepend = function (item/*type:$Node*/) {
                    var swapArr = new Array();
                    swapArr.push(item);
                    for ( var i = 0 ; i < this.queue ; i++ ) {
                        swapArr.push(this.queue[i]);
                    }
                    this.queue = swapArr;
                }
                //refresh angle of node in queue and clear timer control
                this.refresh = function (callback) {
                    if ( this.head != null ) {
                        /*call external function*/
                        this.head.node.find("a:first").unbind("click", doExtraWork);
                        /*end.call external function*/

                        /*call internal function*/
                        this.head.node.find("a:first").unbind("click", parent.zoom);
                    }
                    parent.timerControl.clear();
                    callback();
                }
                //output the queue's node to browser
                this.show = function () {
                    for ( var i = 0 ; i < this.queue.length ; i++ ) {
                        this.queue[i].outputToScreen();
                    }
                }
                //rotate queue by angle
                this.rotateToFront = function (item/*type:$Node*/) {
                    if ( parent.timerControl.isClear() && parent.ready && !parent.isZoom ) {
                        this.refresh(function () {
                            self.head = item;
                            var offsetAngle = item.angle_TO_FRONT;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT - offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                    else if ( parent.timerControl.isClear() && parent.ready && parent.isZoom ) {
                        parent.queue.head.revert(function () {
                            parent.queue.rotateToFront(item);
                        });
                    }
                }
                //rotate queue CW
                this.rotateCW = function () {
                    if ( parent.timerControl.isClear() && parent.ready && !parent.isZoom ) {
                        this.refresh(function () {
                            var offsetAngle = parent.angleX;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT + offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                if ( angle == 0 || angle == 360) {
                                    self.head = self.queue[i];
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                    else if ( parent.timerControl.isClear() && parent.ready && parent.isZoom ) {
                        parent.queue.head.revert(function () {
                            parent.queue.rotateCW();
                        });
                    }
                }
                //rotate queue CCW
                this.rotateCCW = function () {
                    if ( parent.timerControl.isClear() && parent.ready && !parent.isZoom ) {
                        this.refresh(function () {
                            var offsetAngle = -parent.angleX;
                            for ( var i = 0 ; i < self.queue.length ; i++ ) {
                                var angle = self.queue[i].angle_TO_FRONT + offsetAngle;
                                if ( angle > 180 ) {
                                    angle -= 360;
                                }
                                else if ( angle <= -180 ) {
                                    angle += 360;
                                }
                                if ( angle == 0 || angle == 360 ) {
                                    self.head = self.queue[i];
                                }
                                self.queue[i].moveNodeToAngle(angle);
                            }
                        });
                    }
                    else if ( parent.timerControl.isClear() && parent.ready && parent.isZoom ) {
                        parent.queue.head.revert(function () {
                            parent.queue.rotateCCW();
                        });
                    }
                }
            }
            /* TIMER CONTROL */
            $TimerControl = function () {
                this.groupTimer = new Array();
                this.garbageTimer = 0;

                /*methods*/
                //Clear TimerControl
                this.clear = function () {
                    for ( var i = 0 ; i < this.groupTimer.length ; i++ ) {
                        clearInterval(this.groupTimer[i]);
                    }
                    this.groupTimer = new Array();
                    this.garbageTimer = 0;
                }
                //Collect finished timer
                this.collectGarbageTimer = function () {
                    this.garbageTimer++;
                    if ( this.isClear() ) {
                        parent.callback();
                    }
                }
                //isClear: all timer have been collected
                this.isClear = function () {
                    return this.groupTimer.length == this.garbageTimer;
                }
            }

            //Extends function
            jQuery.fn.extend({
                traverse: function (i) {
                    var startItem = this.next();
                    var endItem = typeof(i) == "undefined" ? null : i;
                    var regExp;
                    var items = new Array();
                    if (endItem != null) {
                        do {
                            items.push(startItem);
                            startItem = startItem.next();
                            regExp = startItem.attr("class") != ""
                                     ? new RegExp(startItem.attr("class"), "g")
                                     : null
                        } while ( regExp == null || !(regExp).test(endItem.attr("class")) )
                    }
                    else {
                        do {
                            items.push(startItem);
                            startItem = startItem.next()
                        } while ( startItem.html() != null )
                    }
                    return items;
                }
            });

            //MAIN
            this.vanishingPoint = options.vanishingPoint;
            this.cameraY_OffSet = Math.tan(options.cameraX_Angle)*this.vanishingPoint;
            this.speed = options.speed;
            this.listItem = jEl;
            var classes = this.listItem.attr("class").split(" ");
            for ( var i = 0 ; i < classes.length ; i++ ) {
                if ( classes[i].match(/Theme_/) ) {
                    var theme = "Carousel3D_" + classes[i];
                }
                else {
                    var theme = "Carousel3D_Default";
                }
            }
            this.container = $("<div class=\"" + theme + "\"></div>");
            this.listItem.before(this.container);
            this.listItem.appendTo(this.container);
            //Controler
            this.controller =  $(
                               "<div class=\"Carousel3DController\">\n" +
                               "    <a href=\"#\" title=\"Previous\" class=\"Prev\">Previous</a>" +
                               "    <a href=\"#\" title=\"Next\" class=\"Next\">Next</a>" +
                               "</div>"
                               );
            this.controller.appendTo(this.container);
            //Loading Container
            this.loadingContainer = $("<div class=\"LoadingContainer\"><p></p></div>");
            this.loadingContainer.appendTo(this.container);
            this.loadingContainer.css({
                width: this.listItem.outerWidth(),
                height: this.listItem.outerHeight(),
                position: "absolute",
                top: 0,
                left: parent.listItem.get(0).offsetLeft,
                zIndex: 2000
            });
            this.ready = false;
            this.Radius = this.listItem.outerWidth()/2;
            this.Y_BaseLine = this.listItem.outerHeight()/2;
            this.isZoom = false;
            this.scale = 2;
            /*methods*/
            this.onLoadComplete = function () {
                /*call external function*/
                this.queue.head.node.find("a:first").bind("click", doExtraWork);
                /*end.call external function*/

                /*call internal function*/
                //TODO...
                this.queue.head.node.find("a:first").bind("click", parent.zoom);
                /*end.call internal function*/
            }
            this.callback = function () {
                /*call external function*/
                this.queue.head.node.find("a:first").bind("click", doExtraWork);
                /*end.call external function*/

                /*call internal function*/
                //TODO...
                this.queue.head.node.find("a:first").bind("click", parent.zoom);
                /*end.call internal function*/
            }
            /*internal function*/
            //zoom front node
            this.zoom = function () {
                if ( !parent.isZoom ) {
                    parent.queue.head.shrink(); //shrink : method of NODE
                }
                else {
                    parent.queue.head.revert(); //revert : method of NODE
                }
            }
            //rotateCameraX camera
            this.rotateCameraX = function (angle) {
                this.cameraY_OffSet = Math.tan(angle*Math.PI/180)*this.vanishingPoint;
                for ( var i = 0 ; i < this.queue.queue.length ; i++ ) {
                    this.queue.queue[i].calculate();
                    this.queue.queue[i].outputToScreen();
                }
            }

            //rotate camera_X
            $("#camemaControl").bind("click", function () {
                parent.rotateCameraX(parseInt($("#cameraAngleValue").val()))
            });

            //Timer
            this.timerControl = new $TimerControl();

            //set up list
            parent.listItem.children("li").css({
                opacity: 0
            });
            //setup items in list
            this.listItem.children("li").each(function () {
                var item = $(this);
                item.css({
                    position: "absolute"
                })
            });

            //set up circle
            var items = this.listItem.children("li");
            var images = new Array();
            this.angleX = 360/items.length; //in DEGREE

            //Distribute item onto the circle, except FRONT_NODE (first item)
            this.queue = new $Queue();
            for ( var i = 0 ; i < items.length ; i++ ) {
                var angle = this.angleX*i <= 180
                            ? this.angleX*i
                            : this.angleX*i - 360;
                var sinX = +Math.cos((90+angle)*Math.PI/180);
                var cosX = -Math.sin((90+angle)*Math.PI/180);
                var NODE = new $Node({
                    node: $(items[i]),
                    x: this.Radius*sinX,
                    y: this.Y_BaseLine,
                    z: this.Radius*(1+cosX),
                    angle_TO_FRONT: angle
                });

                //caculation
                NODE.calculate();
                //append to queue
                this.queue.append(NODE);
                //events to FRONT_NODE
                if ( i == 0 ) {
                    this.queue.head = NODE;
                }

                //images list
                images.push($(items[i]).find("img:first").attr("src"));
            }
            //output to screen
            new PreloadImages(images, function () {
                parent.loadingContainer.fadeOut("slow", function() {
                    parent.listItem.children("li").animate({
                        opacity: 1
                    });
                });
                parent.ready = true;
                parent.queue.show();
                parent.onLoadComplete();

            } , this.loadingContainer.find("p:first").get(0));

            //document events
            $(document).bind("click", function (evt) {
                parent.queue.head.revert(function () {
                    parent.isZoom = false;
                });
                evt.stopPropagation();
            });
            //events: NEXT and PREV
            this.controller.find(".Next:first").bind("click", function () {
                parent.queue.rotateCW(); //CW = clockwise
                return false;
            });
            this.controller.find(".Prev:first").bind("click", function () {
                parent.queue.rotateCCW(); //CCW = counter-clockwise
                return false;
            });
        }

        //setup
        $(this).each(function () {
            new $3DCarousel($(this));
        })
    }
});

function doExtraWork () { //onclick event on FRONT NODE
}
